﻿#region License
// Copyright (c) 2013, BlueCrest Studio Ltd
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
// 
// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
// 
// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Security.Policy;
using System.Text.RegularExpressions;
using Microsoft.Practices.Prism;
using Microsoft.Practices.Prism.Modularity;

namespace BlueCrest.PrismExtensions.Modularity
{
    /// <summary>
    /// </summary>
    public class SmartDirectoryModuleCatalog : ModuleCatalog
    {
        /// <summary>
        /// Directory containing modules to search for.
        /// </summary>
        public string ModulePath { get; set; }

        private DirectoryModuleContext _context;

        #region Ctor
        
        public SmartDirectoryModuleCatalog() 
        {
            _context = new DirectoryModuleContext();
            //add all dlls in given ModulePath folder
            _context.IncludedSet.Add(@".*\.dll");
        }

        /// <summary>
        /// Ctor which takes ModulePath and single regex for inclusion Pattern
        /// </summary>
        /// <param name="modulePath"></param>
        /// <param name="inclusionRegexPattern">Regex format (defaults to *.dll)</param>
        /// <param name="exclusionSet"></param>
        public SmartDirectoryModuleCatalog(string modulePath, string inclusionRegexPattern = @".*\.dll", IEnumerable<string> exclusionSet = null)
            : this(modulePath, new List<string>{inclusionRegexPattern}, exclusionSet)
        {
        }


        /// <summary>
        /// Ctor which takes ModulePath and single regex for inclusion Pattern
        /// </summary>
        /// <param name="modulePath"></param>
        /// <param name="inclusionRegexPatternSet">Set of Regex patterns for inclusion</param>
        /// <param name="exclusionSet"></param>
        public SmartDirectoryModuleCatalog(string modulePath, IEnumerable<string> inclusionRegexPatternSet
                            , IEnumerable<string> exclusionSet = null)
        {
            if (inclusionRegexPatternSet == null)
            {
                throw new ArgumentNullException("inclusionRegexPatternSet", "Given parameter is null");
            }

            ModulePath = modulePath;
            _context = new DirectoryModuleContext();
            _context.IncludedSet.AddRange(inclusionRegexPatternSet);

            if (exclusionSet != null)
            {
                _context.ExclusionSet.AddRange(exclusionSet);
            }
        }

        #endregion




        /// <summary>
        /// Drives the main logic of building the child domain and searching for the assemblies.
        /// </summary>
        protected override void InnerLoad()
        {
            if (string.IsNullOrEmpty(ModulePath))
                throw new InvalidOperationException("ModulePath Cannot Be Null Or Empty");

            if (!Directory.Exists(ModulePath))
            {
                throw new InvalidOperationException(string.Format("Directory Not Found:{0}", ModulePath));
            }
            else
            {
                AppDomain domain = BuildChildDomain(AppDomain.CurrentDomain);
                try
                {
                    var list = new List<string>();
                    IEnumerable<string> collection = (AppDomain.CurrentDomain.GetAssemblies()).Where((assembly =>
                        {
                            if (!(assembly is AssemblyBuilder) &&
                                (assembly).GetType().FullName !=
                                "System.Reflection.Emit.InternalAssemblyBuilder")
                            {
                                return !string.IsNullOrEmpty(assembly.Location);
                            }
                            else
                                return false;
                        })).Select((assembly => assembly.Location));

                    list.AddRange(collection);
                    Type type = typeof(InnerModuleInfoLoader);

                    if (!(type.Assembly != null))
                        return;

                    var moduleInfoLoader =
                        (InnerModuleInfoLoader)
                        domain.CreateInstanceFrom(type.Assembly.Location, type.FullName).Unwrap();
                    moduleInfoLoader.LoadAssemblies(list);
                    Items.AddRange(moduleInfoLoader.GetModuleInfos(ModulePath, _context));
                }
                finally
                {
                    AppDomain.Unload(domain);
                }
            }
        }

        /// <summary>
        ///     Creates a new child domain and copies the evidence from a parent domain.
        /// </summary>
        /// <param name="parentDomain">The parent domain.</param>
        /// <returns>
        ///     The new child domain.
        /// </returns>
        /// <remarks>
        ///     Grabs the <paramref name="parentDomain" /> evidence and uses it to construct the new
        ///     <see cref="T:System.AppDomain" /> because in a ClickOnce execution environment, creating an
        ///     <see cref="T:System.AppDomain" /> will by default pick up the partial trust environment of
        ///     the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
        ///     create domain and applies the evidence from the ClickOnce manifests to
        ///     create the domain that the application is actually executing in. This will
        ///     need to be Full Trust for Composite Application Library applications.
        /// </remarks>
        /// <exception cref="T:System.ArgumentNullException">
        ///     An <see cref="T:System.ArgumentNullException" /> is thrown if <paramref name="parentDomain" /> is null.
        /// </exception>
        protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
        {
            if (parentDomain == null)
                throw new ArgumentNullException("parentDomain");
            else
                return AppDomain.CreateDomain("DiscoveryRegion", new Evidence(parentDomain.Evidence),
                                              parentDomain.SetupInformation);
        }

        private class InnerModuleInfoLoader : MarshalByRefObject
        {
            internal IEnumerable<ModuleInfo> GetModuleInfos(string path, DirectoryModuleContext context)
            {
                var directory = new DirectoryInfo(path);
                ResolveEventHandler resolveEventHandler = ((sender, args) => OnReflectionOnlyResolve(args, directory));
                AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;

                Type type =
                    (AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies()).First(
                        (asm => asm.FullName == typeof(IModule).Assembly.FullName)).GetType(typeof(IModule).FullName);
                ModuleInfo[] moduleInfoArray = GetNotAllreadyLoadedModuleInfos(directory, type, context).ToArray();
                AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
                return moduleInfoArray;
            }

            private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(DirectoryInfo directory
                                , Type moduleType
                                , DirectoryModuleContext context)
            {
                var list = new List<FileInfo>();
                Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

                var getFilteredFiles = GetFiles(directory, context);

                var files = getFilteredFiles.Where(
                    (file =>
                     alreadyLoadedAssemblies.FirstOrDefault(
                         (assembly =>
                          string.Compare(Path.GetFileName(assembly.Location), file.Name,
                                         StringComparison.OrdinalIgnoreCase) == 0)) == (Assembly) null));

                foreach (FileInfo fileInfo in files)
                {
                    try
                    {
                        Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName);
                        list.Add(fileInfo);
                    }
                    catch (BadImageFormatException ex)
                    {

                    }
                }
                return list.SelectMany(
                        (file => (Assembly.ReflectionOnlyLoadFrom(file.FullName)
                                .GetExportedTypes())
                                .Where(moduleType.IsAssignableFrom)
                                .Where((t => t != moduleType))
                                .Where((t => !t.IsAbstract))
                                .Select((CreateModuleInfo))));
            }

            private static IEnumerable<FileInfo> GetFiles(DirectoryInfo directory, DirectoryModuleContext context)
            {
                //increase regex cache
                Regex.CacheSize = Math.Max(Regex.CacheSize, context.IncludedSet.Count + context.ExclusionSet.Count);

                //Any match on inclusion set and None (All pattern fail) on exclusion set
                var result = directory.GetFiles("*.dll").Where(fileInfo =>
                            context.IncludedSet.Any(pattern => Regex.Match(fileInfo.Name, pattern).Success)
                            && (context.ExclusionSet.Count == 0 || context.ExclusionSet.All(pattern => !Regex.Match(fileInfo.Name, pattern).Success))
                    );

                return result;
            }

            private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
            {
                Assembly assembly =
                    (AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies()).FirstOrDefault(
                        (asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase)));
                if (assembly != null)
                    return assembly;
                var assemblyName = new AssemblyName(args.Name);
                string str = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
                if (File.Exists(str))
                    return Assembly.ReflectionOnlyLoadFrom(str);
                else
                    return Assembly.ReflectionOnlyLoad(args.Name);
            }

            internal void LoadAssemblies(IEnumerable<string> assemblies)
            {
                foreach (string assemblyFile in assemblies)
                {
                    try
                    {
                        Assembly.ReflectionOnlyLoadFrom(assemblyFile);
                    }
                    catch (FileNotFoundException ex)
                    {
                    }
                }
            }

            private static ModuleInfo CreateModuleInfo(Type type)
            {
                string name = type.Name;
                var list = new List<string>();
                bool flag = false;
                CustomAttributeData customAttributeData1 =
                    (CustomAttributeData.GetCustomAttributes(type)).FirstOrDefault(
                        (cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName));
                if (customAttributeData1 != null)
                {
                    foreach (CustomAttributeNamedArgument attributeNamedArgument in customAttributeData1.NamedArguments)
                    {
                        switch (attributeNamedArgument.MemberInfo.Name)
                        {
                            case "ModuleName":
                                name = (string)attributeNamedArgument.TypedValue.Value;
                                continue;
                            case "OnDemand":
                                flag = (bool)attributeNamedArgument.TypedValue.Value;
                                continue;
                            case "StartupLoaded":
                                flag = !(bool)attributeNamedArgument.TypedValue.Value;
                                continue;
                            default:
                                continue;
                        }
                    }
                }
                foreach (
                    CustomAttributeData customAttributeData2 in
                        (CustomAttributeData.GetCustomAttributes(type)).Where(
                            (cad =>
                             cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName)))
                    list.Add((string)customAttributeData2.ConstructorArguments[0].Value);
                var moduleInfo = new ModuleInfo(name, type.AssemblyQualifiedName)
                    {
                        InitializationMode = flag ? InitializationMode.OnDemand : InitializationMode.WhenAvailable,
                        Ref = type.Assembly.CodeBase
                    };
                moduleInfo.DependsOn.AddRange(list);
                return moduleInfo;
            }
        }
    }
}